--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit ef4c2ae02b5358f6a5949136e05f52b25d0ff61a
Parents : 65abc3f
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-06-02T07:07:45-05:00
refactor(rngit): remove RNGit explorer tool
Changes
4 files changed, 0 insertions(+), 887 deletions(-)
Diff
diff --git a/meshchatx/src/backend/rngit_tool.py b/meshchatx/src/backend/rngit_tool.py
deleted file mode 100644
index f97bf161..00000000
--- a/meshchatx/src/backend/rngit_tool.py
+++ /dev/null
@@ -1,331 +0,0 @@
-# SPDX-License-Identifier: 0BSD
-
-"""RNGit explorer: resolve paths and run ``/git/list`` for candidate repo names."""
-
-from __future__ import annotations
-
-import asyncio
-import base64
-import contextlib
-import re
-from typing import Any
-
-import RNS
-
-RNGIT_ANNOUNCE_ASPECT = "git.repositories"
-
-RNGIT_APP_NAME = "git"
-RNGIT_ASPECT = "repositories"
-RNGIT_PATH_LIST = "/git/list"
-RNGIT_IDX_REPOSITORY = 0x00
-
-_MAX_PROBE_NAMES = 64
-_MAX_REFS_PREVIEW_CHARS = 8000
-
-_MAX_RNGIT_PATH_TIMEOUT_S = 600.0
-_MAX_RNGIT_LINK_TIMEOUT_S = 180.0
-_MAX_RNGIT_LIST_TIMEOUT_S = 600.0
-
-
-def _clamp_timeout(
- value: float | None,
- *,
- default: float,
- minimum: float,
- maximum: float,
-) -> float:
- if value is None:
- return default
- try:
- v = float(value)
- except (TypeError, ValueError):
- return default
- if v < minimum:
- return minimum
- return min(v, maximum)
-
-
-def display_name_from_rngit_app_data(app_data_b64: str | None) -> str | None:
- """Decode optional rngit announce app_data (often a short node label)."""
- if not app_data_b64:
- return None
- try:
- raw = base64.b64decode(app_data_b64)
- except Exception:
- return None
- if not raw:
- return None
- text = raw.decode("utf-8", errors="replace").strip()
- if not text:
- return None
- line = text.splitlines()[0].strip()
- if not line:
- return None
- return line[:120]
-
-
-def normalize_destination_hash_hex(value: str) -> str | None:
- raw = value.strip().lower().replace(":", "")
- if len(raw) != RNS.Reticulum.TRUNCATED_HASHLENGTH // 4:
- return None
- try:
- bytes.fromhex(raw)
- except ValueError:
- return None
- return raw
-
-
-def slug_segment(name: str) -> str | None:
- s = name.strip()
- if not s or len(s) > 256:
- return None
- if not re.fullmatch(r"[A-Za-z0-9._-]+", s):
- return None
- return s
-
-
-def parse_repository_name_lines(
- text: str, *, limit: int = _MAX_PROBE_NAMES
-) -> list[str]:
- out: list[str] = []
- for line in text.splitlines():
- s = line.strip()
- if not s or s.startswith("#"):
- continue
- if len(out) >= limit:
- break
- out.append(s)
- return out
-
-
-def clone_command(destination_hash_hex: str, group: str, repository: str) -> str:
- return f"git clone rns://{destination_hash_hex}/{group}/{repository}"
-
-
-async def list_remote_git_refs(
- identity: RNS.Identity,
- destination_hash_hex: str,
- group_name: str,
- repository_name: str,
- *,
- path_timeout: float | None = None,
- link_timeout: float | None = None,
- list_timeout: float | None = None,
- for_push: bool = False,
-) -> dict[str, Any]:
- """Open an rngit link and perform ``/git/list`` for ``group_name/repository_name``."""
- norm_hash = normalize_destination_hash_hex(destination_hash_hex)
- if not norm_hash:
- return {"ok": False, "error": "invalid_destination_hash"}
- group = slug_segment(group_name)
- repo = slug_segment(repository_name)
- if not group:
- return {"ok": False, "error": "invalid_group_name"}
- if not repo:
- return {"ok": False, "error": "invalid_repository_name"}
-
- path_timeout = _clamp_timeout(
- path_timeout,
- default=float(RNS.Transport.PATH_REQUEST_TIMEOUT),
- minimum=1.0,
- maximum=_MAX_RNGIT_PATH_TIMEOUT_S,
- )
- link_timeout = _clamp_timeout(
- link_timeout,
- default=30.0,
- minimum=1.0,
- maximum=_MAX_RNGIT_LINK_TIMEOUT_S,
- )
- list_timeout = _clamp_timeout(
- list_timeout,
- default=120.0,
- minimum=1.0,
- maximum=_MAX_RNGIT_LIST_TIMEOUT_S,
- )
-
- destination_hash = bytes.fromhex(norm_hash)
-
- loop = asyncio.get_running_loop()
- if not RNS.Transport.has_path(destination_hash):
- RNS.Transport.request_path(destination_hash)
-
- deadline = loop.time() + path_timeout
- while not RNS.Transport.has_path(destination_hash) and loop.time() < deadline:
- await asyncio.sleep(0.1)
-
- if not RNS.Transport.has_path(destination_hash):
- return {"ok": False, "error": "path_not_found"}
-
- remote_identity = RNS.Identity.recall(destination_hash)
- if not remote_identity:
- return {"ok": False, "error": "identity_not_found"}
-
- destination = RNS.Destination(
- remote_identity,
- RNS.Destination.OUT,
- RNS.Destination.SINGLE,
- RNGIT_APP_NAME,
- RNGIT_ASPECT,
- )
- link = RNS.Link(destination)
-
- link_ready = False
- link_failed = False
- done_event = asyncio.Event()
- response_holder: dict[str, Any] = {}
-
- def on_established(lnk: RNS.Link):
- nonlocal link_ready, link_failed
- try:
- lnk.identify(identity)
- link_ready = True
- except Exception:
- link_failed = True
-
- def on_closed(_lnk: RNS.Link):
- nonlocal link_failed
- if not link_ready:
- link_failed = True
-
- link.set_link_established_callback(on_established)
- link.set_link_closed_callback(on_closed)
-
- try:
- deadline = loop.time() + link_timeout
- while not link_ready and not link_failed and loop.time() < deadline:
- await asyncio.sleep(0.1)
-
- if not link_ready or link_failed:
- return {"ok": False, "error": "link_failed"}
-
- repo_path = f"{group}/{repo}"
- request_data: dict[str, Any] = {
- RNGIT_IDX_REPOSITORY: repo_path,
- "for_push": bool(for_push),
- }
-
- def on_response(request_receipt):
- response_holder["response"] = getattr(request_receipt, "response", None)
- loop.call_soon_threadsafe(done_event.set)
-
- def on_failed(_request_receipt=None):
- response_holder["response"] = None
- loop.call_soon_threadsafe(done_event.set)
-
- receipt = link.request(
- RNGIT_PATH_LIST,
- request_data,
- response_callback=on_response,
- failed_callback=on_failed,
- timeout=list_timeout,
- )
- if receipt is False:
- return {"ok": False, "error": "request_not_sent"}
-
- try:
- await asyncio.wait_for(done_event.wait(), timeout=list_timeout + 5.0)
- except TimeoutError:
- return {"ok": False, "error": "list_timeout"}
-
- response = response_holder.get("response")
- if response is None or not isinstance(response, (bytes, bytearray)):
- return {"ok": False, "error": "invalid_response"}
-
- body = bytes(response)
- if len(body) < 1:
- return {"ok": False, "error": "empty_response"}
-
- status_byte = body[0]
- payload = body[1:]
- if status_byte != 0:
- return {
- "ok": False,
- "error": payload.decode("utf-8", errors="replace").strip()
- or "server_refused",
- }
-
- return {"ok": True, "refs": payload.decode("utf-8", errors="replace")}
- finally:
- with contextlib.suppress(Exception):
- link.teardown()
-
-
-async def probe_repositories(
- identity: RNS.Identity,
- destination_hash_hex: str,
- group_name: str,
- repository_names: list[str],
- *,
- path_timeout: float | None = None,
- link_timeout: float | None = None,
- list_timeout: float | None = None,
- for_push: bool = False,
- max_preview_chars: int = _MAX_REFS_PREVIEW_CHARS,
-) -> dict[str, Any]:
- """Try ``/git/list`` for each repository name; used when rngit has no index endpoint."""
- norm = normalize_destination_hash_hex(destination_hash_hex)
- if not norm:
- return {"ok": False, "error": "invalid_destination_hash", "results": []}
- group = slug_segment(group_name)
- if not group:
- return {"ok": False, "error": "invalid_group_name", "results": []}
-
- trimmed = [n.strip() for n in repository_names if n.strip()][:_MAX_PROBE_NAMES]
- if not trimmed:
- return {"ok": False, "error": "no_repository_names", "results": []}
-
- results: list[dict[str, Any]] = []
- for raw in trimmed:
- repo = slug_segment(raw)
- if not repo:
- results.append(
- {
- "repository": raw,
- "reachable": False,
- "error": "invalid_repository_name",
- "clone_command": None,
- },
- )
- continue
-
- listed = await list_remote_git_refs(
- identity,
- norm,
- group,
- repo,
- path_timeout=path_timeout,
- link_timeout=link_timeout,
- list_timeout=list_timeout,
- for_push=for_push,
- )
- cmd = clone_command(norm, group, repo)
- if listed.get("ok"):
- refs = listed.get("refs") or ""
- results.append(
- {
- "repository": repo,
- "reachable": True,
- "refs_preview": refs[:max_preview_chars],
- "refs_truncated": len(refs) > max_preview_chars,
- "clone_command": cmd,
- "error": None,
- },
- )
- else:
- results.append(
- {
- "repository": repo,
- "reachable": False,
- "refs_preview": None,
- "refs_truncated": False,
- "clone_command": cmd,
- "error": listed.get("error"),
- },
- )
-
- return {
- "ok": True,
- "destination_hash": norm,
- "group_name": group,
- "results": results,
- }
diff --git a/meshchatx/src/frontend/components/tools/RNGitExplorerPage.vue b/meshchatx/src/frontend/components/tools/RNGitExplorerPage.vue
deleted file mode 100644
index 7cb00b99..00000000
--- a/meshchatx/src/frontend/components/tools/RNGitExplorerPage.vue
+++ /dev/null
@@ -1,367 +0,0 @@
-<!-- SPDX-License-Identifier: 0BSD -->
-
-<template>
- <div class="flex flex-col flex-1 overflow-hidden min-w-0 bg-slate-50 dark:bg-zinc-950">
- <ToolsPageHeader
- icon="source-branch"
- :title="$t('tools.rngit_explorer.title')"
- :description="$t('tools.rngit_explorer.description')"
- accent="teal"
- />
-
- <div class="flex-1 overflow-y-auto min-w-0">
- <div class="p-3 sm:p-4 md:p-6 max-w-5xl mx-auto space-y-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
- <div
- class="rounded-lg border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-4 space-y-2"
- >
- <div class="flex flex-wrap items-center justify-between gap-2">
- <h2 class="text-sm font-semibold text-gray-900 dark:text-white">
- {{ $t("rngit_explorer.heard_heading") }}
- </h2>
- <button
- type="button"
- class="text-xs text-teal-600 dark:text-teal-400 hover:underline"
- @click="loadHeardAnnounces"
- >
- {{ $t("rngit_explorer.refresh_heard") }}
- </button>
- </div>
- <p class="text-xs text-gray-500 dark:text-zinc-500">
- {{ $t("rngit_explorer.heard_hint") }}
- </p>
- <div v-if="heardLoading" class="text-xs text-gray-500 py-2">{{ $t("common.loading") }}</div>
- <ul
- v-else-if="heardAnnounces.length"
- class="max-h-48 overflow-y-auto divide-y divide-gray-100 dark:divide-zinc-800 rounded-lg border border-gray-100 dark:border-zinc-800"
- >
- <li
- v-for="h in heardAnnounces"
- :key="h.destination_hash"
- class="px-3 py-2 cursor-pointer hover:bg-gray-50 dark:hover:bg-zinc-900/80 text-sm"
- :class="pickedHeardHash === h.destination_hash ? 'bg-teal-50/80 dark:bg-teal-950/30' : ''"
- @click="applyHeardAnnounce(h)"
- >
- <div class="font-medium text-gray-900 dark:text-white truncate">
- {{ heardNodeTitle(h) }}
- </div>
- <div class="font-mono text-[11px] text-gray-500 dark:text-zinc-400 truncate">
- {{ h.destination_hash }}
- </div>
- <div class="text-[10px] text-gray-400 dark:text-zinc-500">
- {{ $t("rngit_explorer.hops_label", { n: h.hops != null ? h.hops : "—" }) }}
- </div>
- </li>
- </ul>
- <p v-else class="text-xs text-gray-500 dark:text-zinc-500 py-1">
- {{ $t("rngit_explorer.heard_empty") }}
- </p>
- </div>
-
- <div
- class="rounded-lg border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-4 space-y-3"
- >
- <p class="text-xs text-gray-600 dark:text-zinc-400 leading-relaxed">
- {{ $t("rngit_explorer.intro") }}
- </p>
- <div class="grid sm:grid-cols-2 gap-3">
- <div>
- <label class="glass-label">{{ $t("rngit_explorer.destination_hash") }}</label>
- <input
- v-model="destinationHash"
- type="text"
- autocomplete="off"
- class="w-full rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-2 text-sm font-mono text-gray-900 dark:text-white"
- :disabled="busy"
- />
- </div>
- <div>
- <label class="glass-label">{{ $t("rngit_explorer.group_name") }}</label>
- <input
- v-model="groupName"
- type="text"
- autocomplete="off"
- class="w-full rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-2 text-sm font-mono text-gray-900 dark:text-white"
- :disabled="busy"
- />
- </div>
- </div>
- <div>
- <label class="glass-label">{{ $t("rngit_explorer.repo_names_label") }}</label>
- <textarea
- v-model="repoNamesText"
- rows="6"
- class="w-full rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-2 text-sm font-mono text-gray-900 dark:text-white"
- :disabled="busy"
- :placeholder="$t('rngit_explorer.repo_names_placeholder')"
- />
- </div>
- <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
- <label
- class="inline-flex items-start gap-2 text-xs text-gray-600 dark:text-zinc-400 max-w-prose"
- >
- <input
- v-model="forPush"
- type="checkbox"
- class="rounded-sm border-gray-300 shrink-0 mt-0.5"
- :disabled="busy"
- />
- <span>{{ $t("rngit_explorer.for_push") }}</span>
- </label>
- <button
- type="button"
- class="inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-teal-600 hover:bg-teal-500 text-white text-sm font-medium disabled:opacity-50 shrink-0 self-start sm:self-auto"
- :disabled="busy || !canProbe"
- @click="runProbe"
- >
- <MaterialDesignIcon v-if="!busy" icon-name="radar" class="size-5" />
- <MaterialDesignIcon v-else icon-name="loading" class="size-5 animate-spin" />
- {{ $t("rngit_explorer.probe") }}
- </button>
- </div>
- </div>
-
- <div
- v-if="results.length"
- class="rounded-lg border border-gray-200 dark:border-zinc-800 overflow-hidden"
- >
- <div
- class="px-3 py-2 border-b border-gray-100 dark:border-zinc-800 text-xs font-semibold text-gray-600 dark:text-zinc-400"
- >
- {{ $t("rngit_explorer.results") }}
- </div>
- <ul class="divide-y divide-gray-100 dark:divide-zinc-800">
- <li
- v-for="row in results"
- :key="row.repository"
- class="px-3 py-2.5 cursor-pointer transition-colors text-sm"
- :class="
- selected && selected.repository === row.repository
- ? 'bg-teal-50 dark:bg-teal-950/40'
- : 'hover:bg-gray-50 dark:hover:bg-zinc-900/80'
- "
- @click="selectRow(row)"
- >
- <div class="flex items-center justify-between gap-2 min-w-0">
- <span class="font-mono font-medium text-gray-900 dark:text-white truncate">{{
- row.repository
- }}</span>
- <span
- class="shrink-0 text-xs font-semibold uppercase"
- :class="
- row.reachable
- ? 'text-emerald-600 dark:text-emerald-400'
- : 'text-gray-400 dark:text-zinc-500'
- "
- >
- {{
- row.reachable
- ? $t("rngit_explorer.reachable")
- : $t("rngit_explorer.unreachable")
- }}
- </span>
- </div>
- <div
- v-if="!row.reachable && row.error"
- class="text-xs text-red-600 dark:text-red-400 mt-1 font-mono truncate"
- >
- {{ row.error }}
- </div>
- </li>
- </ul>
- </div>
-
- <div
- v-if="selected"
- class="rounded-lg border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-4 space-y-3"
- >
- <div class="text-sm font-semibold text-gray-900 dark:text-white">
- {{ $t("rngit_explorer.detail_title", { repo: selected.repository }) }}
- </div>
- <div v-if="selected.reachable && selected.clone_command" class="space-y-2">
- <div class="text-xs text-gray-500 dark:text-zinc-400">
- {{ $t("rngit_explorer.clone_command") }}
- </div>
- <div class="flex flex-wrap gap-2 items-start">
- <pre
- class="flex-1 min-w-0 p-2 rounded-sm bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 overflow-x-auto whitespace-pre-wrap break-all"
- >{{ selected.clone_command }}</pre
- >
- <button
- type="button"
- class="shrink-0 px-3 py-2 rounded-lg border border-gray-300 dark:border-zinc-600 text-sm text-gray-800 dark:text-zinc-200 hover:bg-gray-50 dark:hover:bg-zinc-800"
- @click.stop="copyClone"
- >
- {{ $t("rngit_explorer.copy") }}
- </button>
- </div>
- </div>
- <div v-else class="text-xs text-gray-500 dark:text-zinc-400">
- {{ $t("rngit_explorer.no_clone_hint") }}
- </div>
- <div v-if="selected.reachable && selected.refs_preview" class="space-y-1">
- <div class="text-xs text-gray-500 dark:text-zinc-400">
- {{ $t("rngit_explorer.refs_preview") }}
- <span v-if="selected.refs_truncated" class="text-amber-600 dark:text-amber-400">{{
- $t("rngit_explorer.truncated")
- }}</span>
- </div>
- <pre
- class="p-2 rounded-sm bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 max-h-48 overflow-y-auto whitespace-pre-wrap break-all"
- >{{ selected.refs_preview }}</pre
- >
- </div>
- </div>
-
- <p class="text-center text-[10px] leading-relaxed text-gray-400 dark:text-zinc-500 max-w-prose mx-auto">
- {{ $t("rngit_explorer.experimental_footer") }}
- </p>
- </div>
- </div>
- </div>
-</template>
-
-<script>
-import MaterialDesignIcon from "../MaterialDesignIcon.vue";
-import ToastUtils from "../../js/ToastUtils";
-import WebSocketConnection from "../../js/WebSocketConnection";
-import ToolsPageHeader from "./ToolsPageHeader.vue";
-
-const RNGIT_ASPECT = "git.repositories";
-
-export default {
- name: "RNGitExplorerPage",
- components: { MaterialDesignIcon, ToolsPageHeader },
- data() {
- return {
- heardAnnounces: [],
- heardLoading: false,
- pickedHeardHash: "",
- destinationHash: "",
- groupName: "",
- repoNamesText: "",
- forPush: false,
- busy: false,
- results: [],
- selected: null,
- };
- },
- computed: {
- normalizedDestinationHash() {
- return (this.destinationHash || "").trim().toLowerCase().replace(/:/g, "");
- },
- canProbe() {
- const h = this.normalizedDestinationHash;
- return (
- h.length === 32 &&
- /^[0-9a-f]+$/.test(h) &&
- (this.groupName || "").trim().length > 0 &&
- (this.repoNamesText || "").trim().length > 0
- );
- },
- },
- mounted() {
- WebSocketConnection.on("message", this.onSocketMessage);
- this.loadHeardAnnounces();
- },
- beforeUnmount() {
- WebSocketConnection.off("message", this.onSocketMessage);
- },
- methods: {
- onSocketMessage(event) {
- let json;
- try {
- json = JSON.parse(event.data);
- } catch {
- return;
- }
- if (json.type !== "announce" || !json.announce || json.announce.aspect !== RNGIT_ASPECT) {
- return;
- }
- const a = json.announce;
- const i = this.heardAnnounces.findIndex((x) => x.destination_hash === a.destination_hash);
- if (i >= 0) {
- this.heardAnnounces.splice(i, 1, a);
- } else {
- this.heardAnnounces.unshift(a);
- }
- },
- async loadHeardAnnounces() {
- this.heardLoading = true;
- try {
- const { data } = await window.api.get("/api/v1/announces", {
- params: { aspect: RNGIT_ASPECT, limit: 200 },
- });
- this.heardAnnounces = Array.isArray(data.announces) ? data.announces : [];
- } catch (e) {
- ToastUtils.error(e.response?.data?.message || this.$t("rngit_explorer.heard_load_failed"));
- } finally {
- this.heardLoading = false;
- }
- },
- heardNodeTitle(h) {
- const custom = (h.custom_display_name || "").trim();
- if (custom) {
- return custom;
- }
- const d = (h.display_name || "").trim();
- if (d && d !== "Anonymous Peer") {
- return d;
- }
- const hex = (h.destination_hash || "").trim().toLowerCase();
- if (hex.length >= 8) {
- return this.$t("rngit_explorer.heard_title_short", { prefix: hex.slice(0, 8) });
- }
- return this.$t("rngit_explorer.unnamed_node");
- },
- applyHeardAnnounce(h) {
- this.pickedHeardHash = h.destination_hash || "";
- this.destinationHash = h.destination_hash || "";
- },
- selectRow(row) {
- this.selected = { ...row };
- },
- async copyClone() {
- if (!this.selected?.clone_command) {
- return;
- }
- try {
- await navigator.clipboard.writeText(this.selected.clone_command);
- ToastUtils.success(this.$t("rngit_explorer.copied"));
- } catch {
- ToastUtils.error(this.$t("rngit_explorer.copy_failed"));
- }
- },
- async runProbe() {
- if (!this.canProbe) {
- return;
- }
- this.busy = true;
- this.results = [];
- this.selected = null;
- ToastUtils.loading(this.$t("rngit_explorer.probing"), 0, "rngit-explorer-probe");
- try {
- const { data } = await window.api.post("/api/v1/rngit-tool/probe", {
- destination_hash: this.normalizedDestinationHash,
- group_name: (this.groupName || "").trim(),
- repository_names_text: this.repoNamesText,
- for_push: this.forPush,
- });
- if (data.ok && Array.isArray(data.results)) {
- this.results = data.results;
- const firstOk = data.results.find((r) => r.reachable);
- this.selected = firstOk ? { ...firstOk } : data.results[0] ? { ...data.results[0] } : null;
- ToastUtils.success(this.$t("rngit_explorer.probe_done"));
- } else {
- ToastUtils.error(data.error || this.$t("rngit_explorer.probe_failed"));
- }
- } catch (e) {
- const d = e.response?.data;
- ToastUtils.error(d?.error || d?.message || this.$t("rngit_explorer.probe_failed"));
- } finally {
- ToastUtils.dismiss("rngit-explorer-probe");
- this.busy = false;
- }
- },
- },
-};
-</script>
diff --git a/tests/backend/test_rngit_tool.py b/tests/backend/test_rngit_tool.py
deleted file mode 100644
index bb357911..00000000
--- a/tests/backend/test_rngit_tool.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# SPDX-License-Identifier: 0BSD
-
-from unittest.mock import AsyncMock, MagicMock, patch
-
-import pytest
-
-from meshchatx.src.backend import rngit_tool
-
-
-def test_normalize_destination_hash_hex():
- h = "a" * 32
- assert rngit_tool.normalize_destination_hash_hex(h) == h
- assert rngit_tool.normalize_destination_hash_hex("A" * 32) == h
- assert rngit_tool.normalize_destination_hash_hex("x" * 32) is None
-
-
-def test_parse_repository_name_lines():
- text = " one \n#skip\ntwo\n\nthree"
- assert rngit_tool.parse_repository_name_lines(text) == ["one", "two", "three"]
-
-
-def test_clone_command():
- assert (
- rngit_tool.clone_command("ab" * 16, "g", "r")
- == f"git clone rns://{'ab' * 16}/g/r"
- )
-
-
-def test_display_name_from_rngit_app_data():
- import base64
-
- enc = base64.b64encode(b"My mirror\nextra").decode("ascii")
- assert rngit_tool.display_name_from_rngit_app_data(enc) == "My mirror"
- assert rngit_tool.display_name_from_rngit_app_data(None) is None
-
-
-@pytest.mark.asyncio
-async def test_probe_rejects_invalid_destination():
- ident = MagicMock()
- out = await rngit_tool.probe_repositories(ident, "not-hex", "g", ["a"])
- assert out["ok"] is False
- assert out["error"] == "invalid_destination_hash"
-
-
-@pytest.mark.asyncio
-async def test_probe_rejects_empty_names():
- ident = MagicMock()
- out = await rngit_tool.probe_repositories(ident, "a" * 32, "group", [])
- assert out["ok"] is False
- assert out["error"] == "no_repository_names"
-
-
-@pytest.mark.asyncio
-async def test_probe_calls_list_per_repo():
- ident = MagicMock()
- with patch.object(
- rngit_tool, "list_remote_git_refs", new_callable=AsyncMock
- ) as mock_list:
- mock_list.side_effect = [
- {"ok": True, "refs": "ref1\n"},
- {"ok": False, "error": "Not found"},
- ]
- out = await rngit_tool.probe_repositories(
- ident, "a" * 32, "quad4", ["R1", "R2"]
- )
- assert out["ok"] is True
- assert len(out["results"]) == 2
- assert out["results"][0]["reachable"] is True
- assert out["results"][0]["clone_command"] == f"git clone rns://{'a' * 32}/quad4/R1"
- assert mock_list.await_count == 2
diff --git a/tests/frontend/RNGitExplorerPage.test.js b/tests/frontend/RNGitExplorerPage.test.js
deleted file mode 100644
index aa6656eb..00000000
--- a/tests/frontend/RNGitExplorerPage.test.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import { mount } from "@vue/test-utils";
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { createRouter, createWebHistory } from "vue-router";
-import RNGitExplorerPage from "@/components/tools/RNGitExplorerPage.vue";
-import ToastUtils from "@/js/ToastUtils";
-
-vi.mock("@/js/ToastUtils", () => ({
- default: {
- success: vi.fn(),
- error: vi.fn(),
- loading: vi.fn(),
- dismiss: vi.fn(),
- },
-}));
-
-describe("RNGitExplorerPage.vue", () => {
- let axiosMock;
- const router = createRouter({
- history: createWebHistory(),
- routes: [{ path: "/tools", name: "tools", component: { template: "div" } }],
- });
-
- beforeEach(() => {
- axiosMock = {
- post: vi.fn(),
- get: vi.fn().mockResolvedValue({ data: { announces: [] } }),
- };
- window.api = axiosMock;
- });
-
- afterEach(() => {
- delete window.api;
- vi.clearAllMocks();
- });
-
- const mountPage = () =>
- mount(RNGitExplorerPage, {
- global: {
- plugins: [router],
- mocks: { $t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key) },
- stubs: {
- MaterialDesignIcon: { template: "<span />", props: ["iconName"] },
- RouterLink: { template: "<a><slot /></a>", props: ["to"] },
- },
- },
- });
-
- it("posts probe with textarea names", async () => {
- axiosMock.post.mockResolvedValue({
- data: {
- ok: true,
- results: [
- {
- repository: "MeshChatX",
- reachable: true,
- refs_preview: "abc\tHEAD",
- refs_truncated: false,
- clone_command: "git clone rns://h/quad4/MeshChatX",
- error: null,
- },
- ],
- },
- });
- const wrapper = mountPage();
- await wrapper.setData({
- destinationHash: "a".repeat(32),
- groupName: "quad4",
- repoNamesText: "MeshChatX",
- });
- await wrapper.vm.runProbe();
- expect(axiosMock.post).toHaveBeenCalledWith(
- "/api/v1/rngit-tool/probe",
- expect.objectContaining({
- destination_hash: "a".repeat(32),
- group_name: "quad4",
- repository_names_text: "MeshChatX",
- for_push: false,
- })
- );
- expect(wrapper.vm.results.length).toBe(1);
- expect(ToastUtils.success).toHaveBeenCalled();
- });
-
- it("heardNodeTitle prefers custom_display_name then display_name", () => {
- const wrapper = mountPage();
- expect(
- wrapper.vm.heardNodeTitle({
- custom_display_name: " Mine ",
- display_name: "Other",
- destination_hash: "a".repeat(32),
- })
- ).toBe("Mine");
- expect(
- wrapper.vm.heardNodeTitle({
- custom_display_name: "",
- display_name: " Bob ",
- destination_hash: "b".repeat(32),
- })
- ).toBe("Bob");
- });
-
- it("heardNodeTitle uses short hash when display is Anonymous Peer", () => {
- const wrapper = mountPage();
- expect(
- wrapper.vm.heardNodeTitle({
- display_name: "Anonymous Peer",
- destination_hash: "926baefe13daf5178c174f158dae1b45",
- })
- ).toBe('rngit_explorer.heard_title_short:{"prefix":"926baefe"}');
- });
-
- it("separates for-push checkbox row from probe button", () => {
- const wrapper = mountPage();
- const row = wrapper.find(".flex.flex-col.gap-3.sm\\:flex-row");
- expect(row.exists()).toBe(true);
- expect(row.find('input[type="checkbox"]').exists()).toBe(true);
- expect(row.find("button").exists()).toBe(true);
- });
-});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────